package com.weilus.gateway.filters;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.PathContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.*;

/**
 * spring:
 *   cloud:
 *     gateway:
 *       authentication:
 *       - id: flux-call
 *         auth:
 *         - GET /test/**=authenticated()               #需要认证才能访问
 *         - PUT /test/add=auth2.hasScope('user-info')  #认证且指定域权限才能访问
 *         - /test/other=hasRole('aa,bb')               #认证且指定权限才能访问
 */
@Component
@Slf4j
public class AuthenticationGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthenticationGatewayFilterFactory.Config> {
    public static final OAuth2Authentication NULL_AUTHENTICATION = new OAuth2Authentication(new OAuth2Request(),null);
    private ReactiveRedisTemplate<String,OAuth2Authentication> redisTemplate;
    private AuthProperties properties;

    public AuthenticationGatewayFilterFactory(ReactiveRedisConnectionFactory factory,AuthProperties properties){
        super(Config.class);
        RedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
        RedisSerializationContext.RedisSerializationContextBuilder<String, ?> builder = RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
        RedisSerializationContext context = builder.value(jdkSerializer).build();
        this.redisTemplate = new ReactiveRedisTemplate<>(factory, context);
        this.properties = properties;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            PathContainer path = PathContainer.parsePath(exchange.getRequest().getURI().getRawPath());
            HttpMethod method = exchange.getRequest().getMethod();
            String token  = Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("token")).orElse(exchange.getRequest().getQueryParams().getFirst("token"));
            Route route =(Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            Set<AuthProperties.AuthMatcher> matches = properties.getMatchers(route.getId(),method,path);
            if(!CollectionUtils.isEmpty(matches)){
                if(StringUtils.isEmpty(token))return noPermit(exchange);
                return redisTemplate.opsForValue().get("auth:"+token)
                        .defaultIfEmpty(NULL_AUTHENTICATION)
                        .flatMap(auth-> access(auth, matches)?chain.filter(exchange):noPermit(exchange));
            } else {
                return chain.filter(exchange);
            }
        };
    }

    public Mono<Void> noPermit(ServerWebExchange exchange){
        ServerWebExchangeUtils.setResponseStatus(exchange,HttpStatus.FORBIDDEN);
        return exchange.getResponse().setComplete();
    }

    public boolean access(OAuth2Authentication authentication,Set<AuthProperties.AuthMatcher> matchs){
        for(AuthProperties.AuthMatcher m : matchs){
            if(m.isRequiredAuthenticated() && !authentication.isAuthenticated()){
                log.debug("{} {} Required Authenticated! {}",m.getMethod(),m.getPathPattern(),authentication);
                return false;
            }
            if(!CollectionUtils.isEmpty(m.getRequiredScopes())){
                Set<String> grantedScopes = authentication.getOAuth2Request().getScope();
                boolean access = !CollectionUtils.isEmpty(grantedScopes) &&
                        grantedScopes.stream().anyMatch(grantedScope-> m.getRequiredScopes().contains(grantedScope));
                if(!access){
                    log.debug("{} {} Required Scope: {}",m.getMethod(),m.getPathPattern(),m.getRequiredScopes());
                    return false;
                }
            }
            if(!CollectionUtils.isEmpty(m.getRequiredAuthorities())){
                Collection<GrantedAuthority> grantedRoles = authentication.getAuthorities();
                boolean access = !CollectionUtils.isEmpty(grantedRoles) &&
                        grantedRoles.stream().anyMatch(grantedRole-> m.getRequiredAuthorities().contains(grantedRole));
                if(!access){
                    log.debug("{} {} Required Authorities: {}",m.getMethod(),m.getPathPattern(),m.getRequiredAuthorities());
                    return false;
                }
            }
        }
        return true;
    }

    public static class Config {

    }
}
